/* Copyright (c) 2010, Carl Burch. License information is located in the
 * com.cburch.logisim.Main source code and at www.cburch.com/logisim/. */

package com.cburch.logisim.instance;

import java.awt.Color;
import java.awt.Graphics;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import javax.swing.Icon;

import org.apache.commons.collections15.list.UnmodifiableList;

import com.cburch.logisim.LogisimVersion;
import com.cburch.logisim.circuit.CircuitState;
import com.cburch.logisim.comp.AbstractComponentFactory;
import com.cburch.logisim.comp.Component;
import com.cburch.logisim.comp.ComponentDrawContext;
import com.cburch.logisim.data.Attribute;
import com.cburch.logisim.data.AttributeSet;
import com.cburch.logisim.data.AttributeSets;
import com.cburch.logisim.data.Bounds;
import com.cburch.logisim.data.Direction;
import com.cburch.logisim.data.Location;
import com.cburch.logisim.gui.log.Loggable;
import com.cburch.logisim.tools.Pokable;
import com.cburch.logisim.tools.key.KeyConfigurator;
import com.cburch.logisim.util.Icons;
import com.cburch.logisim.util.StringUtil;

/**
 * Represents a category of components that appear in a circuit. This class
 * and <code>Component</code> share the same sort of relationship as the
 * relation between <em>classes</em> and <em>instances</em> in Java. Normally,
 * there is only one ComponentFactory created for any particular category.
 */
@SuppressWarnings("deprecation")
public abstract class InstanceFactory extends AbstractComponentFactory {
    private String name;
    private String displayName;
    private String defaultToolTip;
    private String iconName;
    private Icon icon;
    private Attribute<?>[] attrs;
    private Object[] defaults;
    private AttributeSet defaultSet;
    private Bounds bounds;
    private List<Port> portList;
    private Attribute<Direction> facingAttribute;
    private Boolean shouldSnap;
    private KeyConfigurator keyConfigurator;
    private Class<? extends InstancePoker> pokerClass;
    private Class<? extends InstanceLogger> loggerClass;

    public InstanceFactory(String name) {
        this(name, StringUtil.constantGetter(name));
    }

    public InstanceFactory(String name, String displayName) {
        this.name = name;
        this.displayName = displayName;
        this.iconName = null;
        this.icon = null;
        this.attrs = null;
        this.defaults = null;
        this.bounds = Bounds.EMPTY_BOUNDS;
        this.portList = Collections.emptyList();
        this.keyConfigurator = null;
        this.facingAttribute = null;
        this.shouldSnap = Boolean.TRUE;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public String getDisplayName() {
        return getDisplayGetter().toString();
    }

    @Override
    public String getDisplayGetter() {
        return displayName;
    }

    public void setIconName(String value) {
        iconName = value;
        icon = null;
    }

    public void setIcon(Icon value) {
        iconName = "";
        icon = value;
    }

    @Override
    public final void paintIcon(ComponentDrawContext context,
            int x, int y, AttributeSet attrs) {
        InstancePainter painter = context.getInstancePainter();
        painter.setFactory(this, attrs);
        Graphics g = painter.getGraphics();
        g.translate(x, y);
        paintIcon(painter);
        g.translate(-x, -y);

        if (painter.getFactory() == null) {
            Icon i = icon;
            if (i == null) {
                String n = iconName;
                if (n != null) {
                    i = Icons.getIcon(n);
                    if (i == null) {
                        n = null;
                    }

                }
            }
            if (i != null) {
                i.paintIcon(context.getDestination(), g, x + 2, y + 2);
            } else {
                super.paintIcon(context, x, y, attrs);
            }
        }
    }

    @Override
    public final Component createComponent(Location loc, AttributeSet attrs) {
        InstanceComponent ret = new InstanceComponent(this, loc, attrs);
        configureNewInstance(ret.getInstance());
        return ret;
    }

    public void setOffsetBounds(Bounds value) {
        bounds = value;
    }

    @Override
    public Bounds getOffsetBounds(AttributeSet attrs) {
        Bounds ret = bounds;
        if (ret == null) {
            throw new RuntimeException("offset bounds unknown: "
                    + "use setOffsetBounds or override getOffsetBounds");
        }
        return ret;
    }

    public boolean contains(Location loc, AttributeSet attrs) {
        Bounds bds = getOffsetBounds(attrs);
        if (bds == null) {
            return false;
        }

        return bds.contains(loc, 1);
    }


    public Attribute<Direction> getFacingAttribute() {
        return facingAttribute;
    }

    public void setFacingAttribute(Attribute<Direction> value) {
        facingAttribute = value;
    }

    public KeyConfigurator getKeyConfigurator() {
        return keyConfigurator;
    }

    public void setKeyConfigurator(KeyConfigurator value) {
        keyConfigurator = value;
    }

    public void setAttributes(Attribute<?>[] attrs, Object[] defaults) {
        this.attrs = attrs;
        this.defaults = defaults;
    }

    @Override
    public AttributeSet createAttributeSet() {
        Attribute<?>[] as = attrs;
        AttributeSet ret = as == null ? AttributeSets.EMPTY : AttributeSets.fixedSet(as, defaults);
        return ret;
    }

    @Override
    public Object getDefaultAttributeValue(Attribute<?> attr, LogisimVersion ver) {
        Attribute<?>[] as = attrs;
        if (as != null) {
            for (int i = 0; i < as.length; i++) {
                if (as[i] == attr) {
                    return defaults[i];
                }
            }
            return null;
        } else {
            AttributeSet dfltSet = defaultSet;
            if (dfltSet == null) {
                dfltSet = createAttributeSet();
                defaultSet = dfltSet;
            }
            return dfltSet.getValue(attr);
        }
    }


    public void setPorts(Port[] ports) {
        portList = UnmodifiableList.decorate(Arrays.asList(ports));
    }

    public void setPorts(List<Port> ports) {
        portList = Collections.unmodifiableList(ports);
    }

    public List<Port> getPorts() {
        return portList;
    }

    public void setDefaultToolTip(String value) {
        defaultToolTip = value;
    }

    public String getDefaultToolTip() {
        return defaultToolTip;
    }

    public void setInstancePoker(Class<? extends InstancePoker> pokerClass) {
        if (isClassOk(pokerClass, InstancePoker.class)) {
            this.pokerClass = pokerClass;
        }
    }

    public void setInstanceLogger(Class<? extends InstanceLogger> loggerClass) {
        if (isClassOk(loggerClass, InstanceLogger.class)) {
            this.loggerClass = loggerClass;
        }
    }

    public void setShouldSnap(boolean value) {
        shouldSnap = Boolean.valueOf(value);
    }

    private boolean isClassOk(Class<?> sub, Class<?> sup) {
        boolean isSub = sup.isAssignableFrom(sub);
        if (!isSub) {
            //OK
            System.err.println(sub.getName() + " must be a subclass of " + sup.getName());
            return false;
        }
        try {
            sub.getConstructor(new Class[0]);
            return true;
        } catch (SecurityException e) {
            //OK
            System.err.println(sub.getName() + " needs its no-args constructor to be public");
        } catch (NoSuchMethodException e) {
            //OK
            System.err.println(sub.getName() + " is missing a no-arguments constructor");
        }
        return true;
    }

    @Override
    public final Object getFeature(Object key, AttributeSet attrs) {
        if (key == FACING_ATTRIBUTE_KEY) {
            return facingAttribute;
        }

        if (key == KeyConfigurator.class) {
            return keyConfigurator;
        }

        if (key == SHOULD_SNAP) {
            return shouldSnap;
        }

        return super.getFeature(key, attrs);
    }

    @Override
    public final void drawGhost(ComponentDrawContext context, Color color,
            int x, int y, AttributeSet attrs) {
        InstancePainter painter = context.getInstancePainter();
        Graphics g = painter.getGraphics();
        g.setColor(color);
        g.translate(x, y);
        painter.setFactory(this, attrs);
        paintGhost(painter);
        g.translate(-x, -y);
        if (painter.getFactory() == null) {
            super.drawGhost(context, color, x, y, attrs);
        }
    }

    public void paintIcon(InstancePainter painter) {
        painter.setFactory(null, null);
    }

    public void paintGhost(InstancePainter painter) {
        painter.setFactory(null, null);
    }

    public abstract void paintInstance(InstancePainter painter);
    public abstract void propagate(InstanceState state);

    // event methods
    protected void configureNewInstance(Instance instance) { }
    protected void instanceAttributeChanged(Instance instance, Attribute<?> attr) { }
    protected Object getInstanceFeature(Instance instance, Object key) {
        if (key == Pokable.class && pokerClass != null) {
            return new InstancePokerAdapter(instance.getComponent(), pokerClass);
        } else if (key == Loggable.class && loggerClass != null) {
            return new InstanceLoggerAdapter(instance.getComponent(), loggerClass);
        } else {
            return null;
        }
    }

    public final InstanceState createInstanceState(CircuitState state, Instance instance) {
        return new InstanceStateImpl(state, instance.getComponent());
    }

    public final InstanceState createInstanceState(CircuitState state, Component comp) {
        return createInstanceState(state, ((InstanceComponent) comp).getInstance());
    }
}
